<!DOCTYPE html>
<html>
<head>
    <title>Zipcode Database</title>
    <script>
        // IndexedDB implementations still use API prefixes
        var indexedDB = window.indexedDB ||    // Use the standard DB API
                window.mozIndexedDB ||             // Or Firefox's early version of it
                window.webkitIndexedDB;            // Or Chrome's early version
        // Firefox does not prefix these two:
        var IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction;
        var IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange;

        // We'll use this function to log any database errors that occur
        function logerr(e) {
            console.log("IndexedDB error" + e.code + ": " + e.message);
        }

        // This function asynchronously obtains the database object (creating and
        // initializing the database if necessary) and passes it to the function f().
        function withDB(f) {
            var request = indexedDB.open("zipcodes"); // Request the zipcode database
            request.onerror = logerr;                 // Log any errors
            request.onsuccess = function () {          // Or call this when done
                var db = request.result;  // The result of the request is the database

                // You can always open a database, even if it doesn't exist.
                // We check the version to find out whether the DB has been created and
                // initialized yet.  If not, we have to go do that. But if the db is
                // already set up, we just pass it to the callback function f().
                if (db.version === "1") f(db);   // If db is inited, pass it to f()
                else initdb(db, f);               // Otherwise initialize it first
            }
        }

        // Given a zip code, find out what city it belongs to and asynchronously
        // pass the name of that city to the specified callback function.
        function lookupCity(zip, callback) {
            withDB(function (db) {
                // Create a transaction object for this query
                var transaction = db.transaction(["zipcodes"],  // Object stores we need
                        IDBTransaction.READ_ONLY, // No updates
                        0);            // No timeout

                // Get the object store from the transaction
                var objects = transaction.objectStore("zipcodes");

                // Now request the object that matches the specified zipcode key.
                // The lines above were synchronous, but this one is async
                var request = objects.get(zip);

                request.onerror = logerr;          // Log any errors that occur
                request.onsuccess = function () {   // Pass the result to this function
                    // The result object is now in the request.result
                    var object = request.result;
                    if (object)  // If we found a match, pass city and state to callback
                        callback(object.city + ", " + object.state);
                    else         // Otherwise, tell the callback that we failed
                        callback("Unknown zip code");
                }
            });
        }

        // Given the name of a city find all zipcodes for all cities (in any state)
        // with that name (case-sensitive).  Asynchronously pass the results, one at
        // a time, to the specified callback function
        function lookupZipcodes(city, callback) {
            withDB(function (db) {
                // As above, we create a transaction and get the object store
                var transaction = db.transaction(["zipcodes"],
                        IDBTransaction.READ_ONLY, 0);
                var store = transaction.objectStore("zipcodes");
                // This time we get the city index of the object store
                var index = store.index("cities");

                // This query is likely to have many results, so we have to use a
                // cursor object to retrieve them all. To create a cursor, we need
                // a range object that represents the range of keys
                var range = new IDBKeyRange.only(city);  // A range with only() one key

                // Everything above has been synchronous.
                // Now we request a cursor, which will be returned asynchronously.
                var request = index.openCursor(range);   // Request the cursor
                request.onerror = logerr;                // Log errors
                request.onsuccess = function () {         // Pass cursor to this function
                    // This event handler will be invoked multiple times, once
                    // for each record that matches the query, and then once more
                    // with a null cursor to indicate that we're done.
                    var cursor = request.result    // The cursor is in request.result
                    if (!cursor) return;           // No cursor means no more results
                    var object = cursor.value      // Get the matching record
                    callback(object);              // Pass it to the callback
                    cursor.continue();             // Ask for the next matching record
                };
            });
        }

        // This function is used by an onchange callback in the document below
        // It makes a DB request and displays the result
        function displayCity(zip) {
            lookupCity(zip, function (s) {
                document.getElementById('city').value = s;
            });
        }

        // This is another onchange callback used in the document below.
        // It makes a DB request and displays the results
        function displayZipcodes(city) {
            var output = document.getElementById("zipcodes");
            output.innerHTML = "Matching zipcodes:";
            lookupZipcodes(city, function (o) {
                var div = document.createElement("div");
                var text = o.zipcode + ": " + o.city + ", " + o.state;
                div.appendChild(document.createTextNode(text));
                output.appendChild(div);
            });
        }

        // Set up the structure of the database and populate it with data, then pass
        // the database to the function f(). withDB() calls this function if the
        // database has not been initialized yet. This is the trickiest part of the
        // program, so we've saved it for last.
        function initdb(db, f) {
            // Downloading zipcode data and storing it in the database can take
            // a while the first time a user runs this application.  So we have to
            // provide notification while that is going on.
            var statusline = document.createElement("div");
            statusline.style.cssText =
                    "position:fixed; left:0px; top:0px; width:100%;" +
                    "color:white; background-color: black; font: bold 18pt sans-serif;" +
                    "padding: 10px; ";
            document.body.appendChild(statusline);
            function status(msg) {
                statusline.innerHTML = msg.toString();
            };

            status("Initializing zipcode database");

            // The only time you can define or alter the structure of an IndexedDB
            // database is in the onsucess handler of a setVersion request.
            var request = db.setVersion("1");       // Try to update the DB version
            request.onerror = status;               // Display status on fail
            request.onsuccess = function () {       // Otherwise call this function
                                                    // Our zipcode database includes only one object store.
                                                    // It will hold objects that look like this: {
                                                    //    zipcode: "02134",   // send it to Zoom! :-)
                                                    //    city: "Allston",
                                                    //    state: "MA",
                                                    //    latitude: "42.355147",
                                                    //    longitude: "-71.13164"
                                                    // }
                                                    //
                                                    // We'll use the "zipcode" property as the database key
                                                    // And we'll also create an index using the city name

                // Create the object store, specifying a name for the store and
                // an options object that includes the "key path" specifying the
                // property name of the key field for this store. (If we omit the
                // key path, IndexedDB will define its own unique integer key.)
                var store = db.createObjectStore("zipcodes", // store name
                        {keyPath: "zipcode"});

                // Now index the object store by city name as well as by zipcode.
                // With this method the key path string is passed directly as a
                // required argument rather than as part of an options object.
                store.createIndex("cities", "city");

                // Now we need to download our zipcode data, parse it into objects
                // and store those objects in object store we created above.
                //
                // Our file of raw data contains lines formatted like this:
                //
                //   02130,Jamaica Plain,MA,42.309998,-71.11171
                //   02131,Roslindale,MA,42.284678,-71.13052
                //   02132,West Roxbury,MA,42.279432,-71.1598
                //   02133,Boston,MA,42.338947,-70.919635
                //   02134,Allston,MA,42.355147,-71.13164
                //
                // Surprisingly, the US Postal Service does not make this data freely
                // available, so we use out-of-date census-based zipcode data from:
                // http://mappinghacks.com/2008/04/28/civicspace-zip-code-database/

                // We use XMLHttpRequest to download the data.  But use the new XHR2
                // onload and onprogress events to process it as it arrives
                var xhr = new XMLHttpRequest();   // An XHR to download the data
                xhr.open("GET", "zipcodes.csv");  // HTTP GET for this URL
                xhr.send();                       // Start right away
                xhr.onerror = status;             // Display any error codes
                var lastChar = 0, numlines = 0;   // How much have we already processed?

                // Handle the database file in chunks as it arrives
                xhr.onprogress = xhr.onload = function (e) {  // Two handlers in one!
                    // We'll process the chunk between lastChar and the last newline
                    // that we've received.  (We need to look for newlines so we don't
                    // process partial records)
                    var lastNewline = xhr.responseText.lastIndexOf("\n");
                    if (lastNewline > lastChar) {
                        var chunk = xhr.responseText.substring(lastChar, lastNewline)
                        lastChar = lastNewline + 1;   // Where to start next time

                        // Now break the new chunk of data into individual lines
                        var lines = chunk.split("\n");
                        numlines += lines.length;

                        // In order to insert zipcode data into the database we need a
                        // transaction object. All the database insertions we make
                        // using this object will be commited to the database when this
                        // function returns and the browser goes back to the event
                        // loop.  To create our transaction object, we need to specify
                        // which object stores we'll be using (we only have one) and we
                        // need to tell it that we'll be doing writes to the database,
                        // not just reads:
                        var transaction = db.transaction(["zipcodes"], // object stores
                                IDBTransaction.READ_WRITE);

                        // Get our object store from the transaction
                        var store = transaction.objectStore("zipcodes");

                        // Now loop through the lines of the zipcode file, create
                        // objects for them, and add them to the object store.
                        for (var i = 0; i < lines.length; i++) {
                            var fields = lines[i].split(","); // Comma-separated values
                            var record = {           // This is the object we'll store
                                zipcode: fields[0],  // All properties are string
                                city: fields[1],
                                state: fields[2],
                                latitude: fields[3],
                                longitude: fields[4]
                            };

                            // The best part about the IndexedDB API is that object
                            // stores are *really* simple.  Here's how we add a record:
                            store.put(record);   // Or use add() to avoid overwriting
                        }

                        status("Initializing zipcode database: loaded "
                                + numlines + " records.");
                    }

                    if (e.type == "load") {
                        // If this was the final load event, then we've sent all our
                        // zipcode data to the database.  But since we've just blasted
                        // it with some 40,000 records, it may still be processing.
                        // So we'll make a simple query. When it succeeds, we know
                        // that the database is ready to go, and we can then remove
                        // the status line and finally call the function f() that was
                        // passed to withDB() so long ago
                        lookupCity("02134", function (s) {  // Allston, MA
                            document.body.removeChild(statusline);
                            withDB(f);
                        });
                    }
                }
            }
        }
    </script>
</head>
<body>
<p>Enter a zip code to find its city:</p>
Zipcode: <input onchange="displayCity(this.value)"></input>
City:
<output id="city"></output>
</div>
<div>
    <p>Enter a city name (case sensitive, without state) to find cities and their zipcodes:</p>
    City: <input onchange="displayZipcodes(this.value)"></input>
    <div id="zipcodes"></div>
</div>
<p><i>This example is only known to work in Firefox 4 and Chrome 11.</i></p>
<p><i>Your first query may take a very long time to complete.</i></p>
<p><i>You may need to start Chrome with --unlimited-quota-for-indexeddb</i></p>
</body>
</html>
